#!/usr/bin/ruby

# This file is part of the aMule project.
#
# Copyright (c) 2003-2011 aMule Team ( admin@amule.org / http://www.amule.org )
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301, USA
#


# This function returns true if a file is filtered
# and shound't be included in the sanity testing.
def IsFiltered(filename)
	(["./config.h", "./configWIN32.h"].index(filename) != nil) or
	(filename =~ /^.\/intl\//) or
	(filename =~ /CryptoPP/)
end



# This class represents lines of code, with line-number and text
# It is used to store the source-files once they have been read
# and afterwards to store the lines returned by the filters.
class Line
	def initialize( number, text )
		@number = number
		@text = text
	end

	attr_reader :number
	attr_reader :text
end



class Result
	def initialize( type, file, line = nil )
		@type = type
		@file = file
		@line = line
	end

	def file_name
		@file.slice( /[^\/]+$/ )
	end

	def file_path
		@file.slice( /^.*\// )
	end

	attr_reader :type
	attr_reader :file
	attr_reader :line
end



# Base class for Sanity Checkers
#
# This class represents the basic sanity-check, which returns all
# files as positive results, regardless of the contents.
class SanityCheck
	def initialize
		@name = "None"
		@title = nil
		@type = "None"
		@desc = "None"
		@results = Array.new
	end

	attr_reader :name
	attr_reader :type
	attr_reader :desc

	def title
		if @title then
			@title
		else
			@name
		end
	end


	def results
		@results
	end

	# This function will be called for each file, with the argument "file" as the
	# name of the file and the argument "lines" being an array of Line objects for
	# each line of the file.
	#
	def parse_file(file, lines)
		raise "Missing parse_file() implementation for Filter: #{@name}"
	end


	private

	def add_results( file, lines = [nil] )
		lines.each do |line|
			@results << Result.new( self, file, line )
		end
	end
end





class CompareAgainstEmptyString < SanityCheck
	def initialize
		super

		@name  = "CmpEmptyString"
		@title = "Comparing With Empty String"
		@type  = "Good Practice"
		@desc  = "Comparisons with empty strings, such as wxT(\"\"), wxEmptyString and "
		@desc += "_(\"\") should be avoided since they force the creation of a temporary "
		@desc += "string object. The proper method is to use the IsEmpty() member-function "
		@desc += "of wxString."
	end

	def parse_file(file, lines)
		results = lines.select do |line|
			line.text =~ /[!=]=\s*(wxEmptyString|wxT\(""\)|_\(""\))/ or
			line.text =~ /(wxEmptyString|wxT\(""\)|_\(""\))\s*[!=]=/
		end

		add_results( file, results )
	end
end



class AssignmentToEmptyString < SanityCheck
	def initialize
		super

		@name  = "EmptyStringAssignment"
		@title = "Assigning The Empty String"
		@type  = "Good Practice"
		@desc  = "Assigning an empty string such as wxT(\"\"), wxEmptyString and _(\"\") "
		@desc += "to a wxString should be avoided, since it forces the creation of a "
		@desc += "temporary object which is assigned to the string. The proper way to "
		@desc += "clear a string is to use the Clear() member-function of wxString."
	end

	def parse_file(file, lines)
		if file =~ /\.cpp$/
			results = lines.select do |line|
				line.text =~ /[^=!]=\s*(wxEmptyString|wxT\(""\)|_\(""\))/
			end

			add_results( file, results )
		end
	end
end



class NoIfNDef < SanityCheck
	def initialize
		super

		@name  = "NoIfNDef"
		@title = "No #ifndef in headerfile"
		@type  = "Good Practice"
		@desc  = "All header files should contain a #ifndef __<FILENAME>__. The purpose is to ensure "
		@desc += "that the header can't be included twice, which would introduce a number of problems."
	end

	def parse_file(file, lines)
		if file =~ /\.h$/ then
			if not lines.find { |x| x.text =~ /^#ifndef.*_H/ } then
				add_results( file )
			end
		end
	end
end



class ThisDeference < SanityCheck
	def initialize
		super

		@name  = "ThisDeference"
		@title = "Dereference of \"this\""
		@type  = "Good Practice"
		@desc  = "In all but the case of templates, using \"this->\" is unnecessary and "
		@desc += "only decreases the readability of the code."
	end

	def parse_file(file, lines)
		results = lines.select do |line|
			line.text =~ /\bthis->/
		end

		add_results( file, results )
	end
end



class Assert < SanityCheck
	def initialize
		super

		@name  = "Assert"
		@type  = "Consistency"
		@desc  = "wxASSERT()s should be used rather than normal assert()s "
		@desc += "for the sake of consistency."
	end

	def parse_file(file, lines)
		results = lines.select do |line|
			line.text =~ /assert\s*\(/
		end

		add_results( file, results )
	end
end



class WxFailUsage < SanityCheck
	def initialize
		super

		@name  = "WxFailUsage"
		@title = "Always failing wxASSERT()"
		@type  = "Good Practice"
		@desc  = "Use wxFAIL_MSG() instead of an always failing wxASSERT()."
	end

	def parse_file(file, lines)
		results = lines.select do |line|
			line.text =~ /\bwxASSERT\s*\(\s*(0|false)\s*\)/ or
			line.text =~ /\bwxASSERT_MSG\s*\(\s*(0|false)\s*,/
		end

		add_results( file, results )
	end
end



class NoMoreWxFail < SanityCheck
	def initialize
		super

		@name  = "NoMoreWxFail"
		@title = "wxFAIL needs an explanation"
		@type  = "Good Practice"
		@desc  = "Use wxFAIL_MSG() instead of wxFAIL to provide a description of what happened. "
		@desc += "Or even better, use one of the wxCHECK*() or wxASSERT*() macros if applicable. "
		@desc += "An assertion of type 'wxAssertFailure' without description does not say much, "
		@desc += "please provide a short explanation of what went wrong."
	end

	def parse_file(file, lines)
		results = lines.select do |line|
			line.text =~ /\bwxFAIL\b/ and not
			line.text =~ /\/\/\s*wxFAIL\b/
		end

		add_results( file, results )
	end
end



class PassByValue < SanityCheck
	def initialize
		super

		@name  = "PassByValue"
		@title = "Pass By Value"
		@type  = "Good Practice"
		@desc  = "Passing objects by value means an extra overhead for large datatypes. "
		@desc += "Therefore these should always be passed by const reference when possible. "
		@desc += "Non-const references should only be used for functions where the function is "
		@desc += "supposed to change the actual value of the argument and return another or no value."
	end

	def parse_file(file, lines)
		results = Array.new

		# Only handle header files
		if file =~ /\.h$/
			# Items that should not be passed by value
			items = [ "wxString", "wxRect", "wxPoint", "CMD4Hash", "CPath" ]

			lines.each do |line|
				# Try to identify function definitions
				if line.text =~ /^\s*(virtual|static|inline|)\s*\w+\s+\w+\s*\(.*\)/
					# Split by arguments
					if line.text =~ /\(.*\)\s*\{/
						args = line.text.match(/\(.*\)\s*\{/)[0]
					else
						args = line.text.match(/\(.*\)/)[0]
					end
					args.split(",").each do |str|
						items.each do |item|
							if str =~ /#{item}\s*[^\s\&\*\(]/
								results.push( line )
							end
						end
					end
				end
			end
		end

		add_results( file, results )
	end
end



class CStr < SanityCheck
	def initialize
		super

		@name  = "CStr"
		@title = "C_Str or GetData"
		@type  = "Unicoding"
		@desc  = "Checks for usage of c_str() or GetData(). Using c_str will often result in "
		@desc += "problems on Unicoded builds and should therefore be avoided. "
		@desc += "Please note that the GetData check isn't that precise, because many other "
		@desc += "classes have GetData members, so it does some crude filtering."
	end

	def parse_file(file, lines)
		results = lines.select do |line|
			if line.text =~ /c_str\(\)/ and line.text !~ /char2unicode\(/
				true
			else
				line.text =~ /GetData\(\)/ and line.text =~ /(wxT\(|wxString|_\()/
			end
		end

		add_results( file, results )
	end
end



class IfNotDefined < SanityCheck
	def initialize
		super

		@name  = "IfDefined"
		@title = "#if (!)defined"
		@type  = "Consistency"
		@desc  = "Use #ifndef or #ifdef instead for reasons of simplicity."
	end

	def parse_file(file, lines)
		results = lines.select do |line|
			if line.text =~ /^#if.*[\!]?defined\(/
				not line.text =~ /(\&\&|\|\|)/
			end
		end

		add_results( file, results )
	end
end



class GPLLicense < SanityCheck
	def initialize
		super

		@name  = "MissingGPL"
		@title = "Missing GPL License"
		@type  = "License"
		@desc  = "All header files should contain the proper GPL blorb."
	end

	def parse_file(file, lines)
		if file =~ /\.h$/
			if lines.find { |x| x.text =~ /This (program|library) is free software;/ } == nil
				add_results( file )
			end
		end
	end
end



class Copyright < SanityCheck
	def initialize
		super

		@name  = "MissingCopyright"
		@title = "Missing Copyright Notice"
		@type  = "License"
		@desc  = "All files should contain the proper Copyright notice."
	end

	def parse_file(file, lines)
		if file =~ /\.h$/
			found = lines.select do |line|
				line.text =~ /Copyright\s*\([cC]\)\s*[-\d,]+ aMule (Project|Team)/
			end

			if found.empty? then
				add_results( file )
			end
		end
	end
end



class PartOfAmule < SanityCheck
	def initialize
		super

		@name  = "aMuleNotice"
		@title = "Missing aMule notice"
		@type  = "License"
		@desc  = "All files should contain a notice that they are part of the aMule project."
	end

	def parse_file(file, lines)
		if file =~ /\.h$/
			found = lines.select do |line|
				line.text =~ /This file is part of the aMule Project/ or
				line.text =~ /This file is part of aMule/
			end

			if found.empty? then
				add_results( file )
			end
		end
	end
end



class MissingBody < SanityCheck
	def initialize
		super

		@name  = "MissingBody"
		@title = "Missing Body in Loop"
		@type  = "Garbage"
		@desc  = "This check looks for loops without any body. For example \"while(true);\" "
		@desc += "In most cases this is a sign of either useless code or bugs. Only in a few "
		@desc += "cases is it valid code, and in those it can often be represented clearer "
		@desc += "in other ways."
	end

	def parse_file(file, lines)
		results = lines.select do |line|
			if line.text =~ /^[^}]*while\s*\(.*\)\s*;/ or
			   line.text =~ /^\s*for\s*\(.*\)\s*;[^\)]*$/
				# Avoid returning "for" spanning multiple lines
				# TODO A better way to count instances
				line.text.split("(").size == line.text.split(")").size
			else
				false
			end
		end

		add_results( file, results )
	end
end



class Translation < SanityCheck
	def initialize
		super

		@name  = "Translation"
		@type  = "Consistency"
		@desc  = "Calls to AddLogLine should translate the message, whereas "
		@desc += "calls to AddDebugLogLine shouldn't. This is because the user "
		@desc += "is meant to see normal log lines, whereas the the debug-lines "
		@desc += "are only meant for the developers and I don't know about you, but "
		@desc += "I don't plan on learning every language we choose to translate "
		@desc += "aMule to. :P"
	end

	def parse_file(file, lines)
		results = lines.select do |line|
			if line.text =~ /\"/
				line.text =~ /AddLogLine.?.?\(.*wxT\(/ or
				line.text =~ /AddDebugLogLine.?\(.*_\(/
			else
				false
			end
		end

		add_results( file, results )
	end
end



class IfZero < SanityCheck
	def initialize
		super

		@name  = "PreIfConstant"
		@title = "#if 0-9"
		@type  = "Garbage"
		@desc  = "Disabled code should be removed as soon as possible. If you wish to disable code "
		@desc += "for only a short period, then please add a comment before the #if. Code with #if [1-9] "
		@desc += "should be left, but the #ifs removed unless there is a pressing need for them."
	end

	def parse_file(file, lines)
		results = lines.select do |line|
			line.text =~ /#if\s+[0-9]/
		end

		add_results( file, results )
	end
end



class InlinedIfTrue < SanityCheck
	def initialize
		super

		@name  = "InlinedIf"
		@title = "Inlined If true/false"
		@type  = "Garbage"
		@desc  = "Using variations of (x ? true : false) or (x ? false : true) is just plain stupid."
	end

	def parse_file(file, lines)
		results = lines.select do |line|
			line.text =~ /\?\s*(true|false)\s*:\s*(true|false)/i
		end

		add_results( file, results )
	end
end



class LoopOnConstant < SanityCheck
	def initialize
		super

		@name  = "LoopingOnConstant"
		@title = "Looping On Constant"
		@type  = "Garbage"
		@desc  = "This checks detects loops that evaluate constant values "
		@desc += "(true,false,0..) or \"for\" loops with no conditionals. all "
		@desc += "are often a sign of poor code and in most cases can be avoided "
		@desc += "or replaced with more elegant code."
	end

	def parse_file(file, lines)
		results = lines.select do |line|
			line.text =~ /while\s*\(\s*([0-9]+|true|false)\s*\)/i or
			line.text =~ /for\s*\([^;]*;\s*;[^;]*\)/
		end

		add_results( file, results )
	end
end



class MissingImplementation < SanityCheck
	def initialize
		super

		@name  = "MissingImplementation"
		@title = "Missing Function Implementation"
		@type  = "Garbage"
		@desc  = "Forgetting to remove a function-definition after having removed it "
		@desc += "from the .cpp file only leads to cluttered header files. The only "
		@desc += "case where non-implemented functions should be used is when it is "
		@desc += "necessary to prevent usage of for instance assignment between "
		@desc += "instances of a class."

		@declarations = Array.new
		@definitions = Array.new
	end

	def results
		@definitions.each do |definition|
			@declarations.delete_if do |pair|
				pair.first == definition
			end
		end


		return @declarations.map do |pair|
			Result.new( self, pair.last.first, pair.last.last )
		end
	end

	def parse_file(file, lines)
		if file =~ /\.h$/ then
			level = 0
			tree = Array.new

			lines.each do |line|
				level += line.text.count( "{" ) - line.text.count( "}" )

				tree.delete_if do |struct|
					struct.first > level
				end


				if line.text !~ /^\s*\/\// and line.text !~ /^\s*#/ and line.text !~ /typedef/ then
					if line.text =~ /^\s*(class|struct)\s+/ and line.text.count( ";" ) == 0 then
						cur_level = level;

						if line.text.count( "{" ) == 0 then
							cur_level += 1
						end

						name = line.text.scan( /^\s*(class|struct)\s+([^\:{;]+)/ )
						if name != [] then
							name = name.first.last.strip
						else
							name = "Unknown at line " + line.number.to_s + " in " + file
						end

						tree << [ cur_level, name ]
					elsif line.text =~ /;\s*$/ and line.text.count( "{" ) == 0 then
						# No pure virtual functions and no return(blah) calls (which otherwise can fit the requirements)
						if line.text !~ /=\s*0\s*;\s*$/ and line.text !~ /return/ then
							re = /^\s*(virtual\s+|static\s+|inline\s+|)\w+(\s+[\*\&]?|[\*\&]\s+)(\w+)\(/.match( line.text )

							if re and level > 0 and tree.last then
								@declarations << [ tree.last.last + "::" + re[ re.length - 1 ], [ file, line ] ]
							end
						end
					end
				end
			end
		else
			lines.each do |line|
				if line.text =~ /\b\w+::\w+\s*\([^;]+$/
					@definitions << line.text.scan( /\b(\w+::\w+)\s*\([^;]+$/ ).first.first
				end
			end
		end
	end
end







# List of enabled filters
filterList = Array.new
filterList.push CompareAgainstEmptyString.new
filterList.push AssignmentToEmptyString.new
filterList.push NoIfNDef.new
filterList.push ThisDeference.new
filterList.push Assert.new
filterList.push PassByValue.new
filterList.push CStr.new
filterList.push IfNotDefined.new
filterList.push GPLLicense.new
filterList.push Copyright.new
filterList.push PartOfAmule.new
filterList.push MissingBody.new
filterList.push Translation.new
filterList.push IfZero.new
filterList.push InlinedIfTrue.new
filterList.push LoopOnConstant.new
filterList.push MissingImplementation.new
filterList.push WxFailUsage.new
filterList.push NoMoreWxFail.new


# Sort enabled filters by type and name. The reason why this is done here is
# because it's much easier than manually resorting every time I add a filter
# or change the name or type of an existing filter.
filterList.sort! do |x,y|
	cmp = x.type <=> y.type

	if cmp == 0 then
		x.title <=> y.title
	else
		cmp
	end
end



def parse_files( path, filters )
	filters = filters.dup

	require "find"

	Find.find( path ) do |filename|
		if filename =~ /\.(cpp|h)$/ and not IsFiltered(filename) then
			File.open(filename, "r") do |aFile|
				# Read lines and add line-numbers
				lines = Array.new
				aFile.each_line do |line|
					lines.push( Line.new( aFile.lineno, line ) )
				end

				lines.freeze

				# Check the file against each filter
				filters.each do |filter|
					# Process the file with this filter
					filter.parse_file( filename, lines )
				end
			end
		end
	end

	results = Array.new
	filters.each do |filter|
		results += filter.results
	end

	results
end




# Helper-function
def get_val( key, list )
	if not list.last or list.last.first != key then
		list << [ key, Array.new ]
	end

	list.last.last
end



def create_result_tree( path, filters )
	# Gather the results
	results = parse_files( path, filters )

	# Sort the results by the following sequence of variables: Path -> File -> Filter -> Line
	results.sort! do |a, b|
		if (a.file_path <=> b.file_path) == 0 then
			if (a.file_name <=> b.file_name) == 0 then
				if (a.type.title <=> b.type.title) == 0 then
					a.line.number <=> b.line.number
				else
					a.type.title <=> b.type.title
				end
			else
				a.file_name <=> b.file_name
			end
		else
			a.file_path <=> b.file_path
		end
	end


	# Create a tree of results: [ Path, [ File, [ Filter, [ Line ] ] ] ]
	result_tree = Array.new
	results.each do |result|
		get_val( result.type, get_val( result.file_name, get_val( result.file_path, result_tree ) ) ) << result
	end


	result_tree
end



def create_filter_tree( filters )
	# Change the filterList to a tree: [ Type, [ Filter ] ]
	filter_tree = Array.new

	filters.each do |filter|
		get_val( filter.type, filter_tree ) << filter
	end

	filter_tree
end



# Converts a number to a string and pads with zeros so that length becomes at least 5
def PadNum( number )
	num = number.to_s

	if ( num.size < 5 )
		( "0" * ( 5 - num.size ) ) + num
	else
		num
	end
end



# Helper-function that escapes some chars to HTML codes
def HTMLEsc( str )
	str.gsub!( /\&/, "&amp;"  )
	str.gsub!( /\"/, "&quot;" )
	str.gsub!( /</,  "&lt;"   )
	str.gsub!( />/,  "&gt;"   )
	str.gsub!( /\n/, "<br>"   )
	str.gsub( /\t/,  "&nbsp;" )
end



# Fugly output code goes here
# ... Abandon hope, yee who read past here
# TODO Enable use of templates.
# TODO Abandon hope.
def OutputHTML( filters, results )
	text  =
"<html>
	<head>
		<STYLE TYPE=\"text/css\">
		<!--
			.dir {
				background-color: \#A0A0A0;
				padding-left: 10pt;
				padding-right: 10pt;
			}
			.file {
				background-color: \#838383;
				padding-left: 10pt;
				padding-right: 10pt;
			}
			.filter {
				background-color: #757575;
				padding-left: 10pt;
				padding-right: 10pt;
				padding-top: 5pt;
			}
		-->
		</STYLE>
	</head>
	<body bgcolor=\"\#BDBDBD\">

		<h1>Filters</h1>
		<dl>
"
	# List the filters
	filters.each do |filterType|
		text +=
"			<dt><b>#{filterType.first}</b></dt>
			<dd>
				<dl>
"
		filterType.last.each do |filter|
			text +=
"					<dt id=\"#{filter.name}\"><i>#{filter.title}</i></dt>
					<dd>
						#{HTMLEsc(filter.desc)}<p>
					</dd>
"
		end

		text +=
"				</dl>
			</dd>
"
	end

	text +=
"		</dl>

		<p>

		<h1>Directories</h1>
		<ul>
"

	# List the directories
	results.each do |dir|
		text +=
"			<li>
				<a href=\"\##{dir.first}\">#{dir.first}</a>
			</li>
"
	end

	text +=
"		</ul>

		<p>

		<h1>Results</h1>
"

	results.each do |dir|
		text +=
"		<div class=\"dir\">
			<h2 id=\"#{dir.first}\">#{dir.first}</h2>
"

		dir.last.each do |file|
			text +=
"			<div class=\"file\">
				<h3>#{file.first}</h3>

				<ul>
"

			file.last.each do |filter|
				text +=
"					<li>
						<div class=\"filter\">
							<b><a href=\"\##{filter.first.name}\">#{filter.first.title}</a></b>

							<ul>
"

				filter.last.each do |result|
					if result.line then
						text +=
"								<li><b>#{PadNum(result.line.number)}:</b> #{HTMLEsc(result.line.text.strip)}</li>
"
					end
				end

				text +=
"							</ul>
						</div>
					</li>
"
			end
			text +=
"				</ul>
			</div>
"
		end

		text +=
"		</div>

		<p>
"
	end

	text +=
"	</body>
</html>"

	return text;
end



# Columnizing, using the http://www.rubygarden.org/ruby?UsingTestUnit example because I'm lazy
# TODO Rewrite it to better support newlines and stuff
def Columnize( text, width, indent )
	return indent + text.scan(/(.{1,#{width}})(?: |$)/).join("\n#{indent}")
end



# Fugly output code also goes here, this is a bit more sparse than the HTML stuff
def OutputTEXT( filters, results )

	# List the filters
	text = "Filters\n"
	filters.each do |filterType|
		text += "\t* #{filterType.first}\n"

		filterType.last.each do |filter|
			text += "\t\t- #{filter.title}\n"

			text += Columnize( filter.desc, 80, "\t\t\t" ) + "\n\n"
		end
	end

	# List the directories
	text += "\n\nDirectories\n"
	results.each do |dir|
		text += "\t#{dir.first}\n"
	end

	text += "\n\nResults\n"

	# To avoid bad readability, I only use fullpaths here instead of sections per dir
	results.each do |dir|
		dir.last.each do |file|
			text += "\t#{dir.first}#{file.first}\n"

			file.last.each do |filter|
				text += "\t\t* #{filter.first.title}\n"

				filter.last.each do |result|
					if result.line then
						text += "\t\t\t#{PadNum(result.line.number)}: #{result.line.text.strip}\n"
					end
				end
			end

			text += "\n"
		end
	end

	return text;
end



#TODO Improved parameter-handling, add =<file> for the outputing to a file
ARGV.each do |param|
	case param
		when "--text" then
			puts OutputTEXT( create_filter_tree( filterList ), create_result_tree( ".", filterList ) )
		when "--html" then
			puts OutputHTML( create_filter_tree( filterList ), create_result_tree( ".", filterList ) )
	end
end
